#include "limbo/errc.h"
#include "limbo/slip/escape.hpp"
#include "limbo/slip/state.hpp"
#include "test-utils.h"

using namespace limbo;

using State = slip::State<void>;
using Context = typename State::Context;
using Packet = typename State::Packet;

TEST_CASE("offsets in unescaping", "[slip]") {
  auto state = State();

  SECTION("normal in") {
    unsigned char buff_raw[] = {'a'};
    auto buff = Chunk(buff_raw, sizeof(buff_raw));

    auto r = slip::unescape(buff, &state);
    REQUIRE(!r);
    CHECK(r.state() == &state);
    CHECK(r.error_code() == (uint32_t)Errc::slip_incomplete);
    CHECK(state.position == 1);
    CHECK(state.offset == 0);
  }

  SECTION("just esc in") {
    unsigned char buff_raw[] = {0xDB};
    auto buff = Chunk(buff_raw, sizeof(buff_raw));

    auto r = slip::unescape(buff, &state);
    REQUIRE(!r);
    CHECK(r.state() == &state);
    CHECK(r.error_code() == (uint32_t)Errc::slip_incomplete);
    CHECK(state.position == 0);
    CHECK(state.offset == 1);
  }

  SECTION("just esc + esc_end in") {
    unsigned char buff_raw[] = {0xDB, 0xDD};
    auto buff = Chunk(buff_raw, 1);

    auto r = slip::unescape(buff, &state);
    REQUIRE(!r);
    CHECK(r.state() == &state);
    CHECK(r.error_code() == (uint32_t)Errc::slip_incomplete);
    CHECK(state.position == 0);
    CHECK(state.offset == 1);

    buff = Chunk(buff_raw, 2);
    r = slip::unescape(buff, &state);
    REQUIRE(!r);
    CHECK(r.state() == &state);
    CHECK(r.error_code() == (uint32_t)Errc::slip_incomplete);
    CHECK(state.position == 1);
    CHECK(state.offset == 0);
  }
}

TEST_CASE("by-byte parsing", "[slip]") {
  auto state = State();
  unsigned char payload_raw[] = {0xC0, 'a', 'b', 0xDB, 0xDB, 'c', 0xC0};
  unsigned char input_buff_raw[50];
  auto payload = Chunk(payload_raw, sizeof(payload_raw));
  auto input_buff = Chunk(input_buff_raw, sizeof(input_buff_raw));
  auto result = slip::escape(input_buff, payload, &state);
  REQUIRE(result);
  REQUIRE(!result.demand());
  auto input_escaped = result.consumed();

  unsigned char output_buff_raw[50];
  auto o = output_buff_raw;
  auto i = input_buff_raw;
  for (uint32_t c = 0; c < input_escaped.size(); c++) {
    o[state.position + state.offset] = *i++;
    auto ptr = output_buff_raw + state.position;
    auto sub_buff = Chunk(ptr, state.offset + 1);
    auto r = slip::unescape(sub_buff, &state);
    if (!r) {
      REQUIRE(r.error_code() == (uint32_t)Errc::slip_incomplete);
    } else {
      CHECK(state.get_parsed().payload == payload);
      CHECK(state.position == 0);
      CHECK(state.offset == 0);
      break;
    }
  }
}

TEST_CASE("escaping", "[slip]") {
  auto state = State();

  SECTION("fully avaialable") {
    SECTION("simple") {
      unsigned char sample_raw[] = {'a', 'b', 'c', 0, 0xC0};
      unsigned char buff_raw[] = {'a', 'b', 'c', 0, 0};
      unsigned char buff_orig_raw[] = {'a', 'b', 'c', 0, 0};
      auto buff = Chunk(buff_raw, sizeof(buff_raw));
      auto data = Chunk(buff_raw, sizeof(buff_raw) - 1);
      auto data_orig = Chunk(buff_orig_raw, data.size());
      auto sample = Chunk(sample_raw, sizeof(buff_raw));
      auto result = slip::escape(buff, data, &state);
      REQUIRE(result);
      CHECK(buff == sample);
      CHECK(result.consumed() == sample);

      auto unescaped = slip::unescape(buff, &state);
      REQUIRE(unescaped);
      CHECK(unescaped.state() == &state);
      CHECK(unescaped.consumed() == buff);
      CHECK(state.packet.payload == data_orig);
      CHECK(state.position == 0);
    }

    SECTION("payload with sequences to be escaped") {
      unsigned char sample_raw[] = {0xDB, 0xDC, 0xDB, 0xDD, 'a',
                                    'b',  'c',  0,    0xC0};
      unsigned char buff_raw[] = {0xC0, 0xDB, 'a', 'b', 'c', 0, 0, 0, 0};
      unsigned char buff_orig_raw[] = {0xC0, 0xDB, 'a', 'b', 'c', 0, 0, 0, 0};
      auto buff = Chunk(buff_raw, sizeof(buff_raw));
      static_assert(sizeof(buff_raw) == sizeof(sample_raw));
      assert(sizeof(sample_raw) == 9);
      assert(buff.size() == 9);
      auto data = Chunk(buff_raw, 6);
      auto data_orig = Chunk(buff_orig_raw, data.size());
      auto sample = Chunk(sample_raw, sizeof(buff_raw));
      auto result = slip::escape(buff, data, &state);
      REQUIRE(result);
      CHECK(buff == sample);
      CHECK(result.consumed() == sample);

      auto unescaped = slip::unescape(buff, &state);
      REQUIRE(unescaped);
      CHECK(unescaped.state() == &state);
      CHECK(unescaped.consumed() == buff);
      CHECK(state.packet.payload == data_orig);
      CHECK(state.position == 0);
    }

    SECTION("payload outside of buff") {
      unsigned char sample_raw[] = {'a', 'b', 'c', 0, 0xC0};
      unsigned char buff_raw[] = {0, 0, 0, 0, 0};
      unsigned char buff_orig_raw[] = {'a', 'b', 'c', 0, 0};
      auto buff = Chunk(buff_raw, sizeof(buff_raw));
      auto data = Chunk(buff_orig_raw, sizeof(buff_orig_raw) - 1);
      auto data_orig = Chunk(buff_orig_raw, data.size());
      auto sample = Chunk(sample_raw, sizeof(buff_raw));
      auto result = slip::escape(buff, data, &state);
      REQUIRE(result);
      CHECK(buff == sample);
      CHECK(result.consumed() == sample);

      auto unescaped = slip::unescape(buff, &state);
      REQUIRE(unescaped);
      CHECK(unescaped.state() == &state);
      CHECK(unescaped.consumed() == buff);
      CHECK(state.packet.payload == data_orig);
      CHECK(state.position == 0);
    }
  }
  SECTION("partially available") {
    unsigned char sample_raw[] = {0xC0, 'a', 'b', 'c', 0xC0};
    unsigned char buff_raw[50];
    auto buff = Chunk(buff_raw, sizeof(buff_raw));
    auto data = Chunk(sample_raw, sizeof(sample_raw));
    auto result = slip::escape(buff, data, &state);
    REQUIRE(result);
    auto consumed = result.consumed();
    auto sz = consumed.size();

    uint32_t i = 0;
    while (true) {
      auto sub_buff = Chunk(buff_raw, i);
      auto r = slip::unescape(sub_buff, &state);
      if (!r) {
        REQUIRE(r.error_code() == (uint32_t)Errc::slip_incomplete);
        auto delta = r.consumed().size() - state.position;
        if (delta) {
          auto o = buff_raw + state.position;
          auto s = o + delta;
          while (s < buff.data() + buff.size()) {
            *o++ = *s++;
          }
          sz -= delta;
        } else {
          ++i;
        }
      } else {
        CHECK(state.get_parsed().payload == data);
        CHECK(state.position == 0);
        REQUIRE(r.consumed().size() == data.size() + 1);
        break;
      }
    }
  }
}

uint8_t example_raw[] = {'H', 'e', 'l', 'l', 'o', 0xC0};
auto example_chunk = Chunk(example_raw, sizeof(example_raw));

uint8_t payload[] = {'H', 'e', 'l', 'l', 'o'};
auto payload_chunk = Chunk(payload, sizeof(payload));

TEST_CASE("send", "[slip]") {
  char buff[sizeof(example_raw)];
  auto buff_chunk = Chunk(buff, sizeof(example_raw));
  auto ctx = Context{};
  auto state = State();

  SECTION("no enough space") {
    auto zero_chunk = Chunk(buff, 0);
    auto result = Packet::send(state, ctx, zero_chunk, payload_chunk);
    REQUIRE(!result);
    CHECK(result.state() == &state);
    CHECK(result.error_code() == (uint32_t)Errc::no_enough_space);
  }

  auto result = Packet::send(state, ctx, buff_chunk, payload_chunk);
  REQUIRE(result);
  auto res_buff = result.consumed();
  CHECK((void *)res_buff.data() == (void *)buff);
  CHECK(res_buff.size() == example_chunk.size());
  CHECK(res_buff == example_chunk);
}

TEST_CASE("recv/success", "[slip]") {
  auto state = State();
  auto result = state.recv(example_chunk, nullptr);
  CHECK(result);
  CHECK(result.consumed() == example_chunk);
  CHECK(result.state() == &state);
  auto &packet = state.get_parsed();
  CHECK(packet.payload == payload_chunk);
}

TEST_CASE("recv empty block /success", "[slip]") {
  uint8_t example_raw[] = {0xC0};
  auto example_chunk = Chunk(example_raw, sizeof(example_raw));

  auto state = State();
  auto result = state.recv(example_chunk, nullptr);
  CHECK(result);
  CHECK(result.consumed() == example_chunk);
  CHECK(result.state() == &state);
  auto &packet = state.get_parsed();
  CHECK(packet.payload.size() == 0);
}
